/*
 * Copyright 2010 Francisco Modesto
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.googlecode.gwt.tinyaop.rebind;

import java.io.PrintWriter;
import java.lang.annotation.Annotation;
import java.util.ArrayList;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;
import com.googlecode.gwt.tinyaop.client.Interceptable;
import com.googlecode.gwt.tinyaop.client.InterceptableProxy;
import com.googlecode.gwt.tinyaop.client.Interceptor;
import com.googlecode.gwt.tinyaop.client.Invocation;
import com.googlecode.gwt.tinyaop.client.MethodDescriptor;

public class InterfaceProxyCreator extends AbstractCreator {
  private static final String RPC_ASYNC = "Async";
  
  public InterfaceProxyCreator(JClassType type, Class<? extends Annotation>[] custom) {
    super(type, custom);
  }

  @Override
  public String create(TreeLogger logger, GeneratorContext context) {
    String pkgeName = classType.getPackage().getName();
    String className = getGeneratedName();
    
    PrintWriter writer = context.tryCreate(logger, pkgeName, className);
    if (writer != null) {
      ClassSourceFileComposerFactory factory = createFactory(pkgeName, className);
      SourceWriter src = factory.createSourceWriter(context, writer);
      createBody(src);
      //src.println("CRASH"); // Is there a better way to know what is going on?
      src.commit(logger);
    }
    return pkgeName + "." + className;
  }

  private ClassSourceFileComposerFactory createFactory(String pkge, String name) {
    ClassSourceFileComposerFactory factory = new ClassSourceFileComposerFactory(pkge, name);
    factory.addImport(GWT.class.getCanonicalName());
    factory.addImport(Interceptor.class.getCanonicalName());
    factory.addImport(Invocation.class.getCanonicalName());
    factory.addImport(MethodDescriptor.class.getCanonicalName());
    factory.addImport(InterceptableProxy.class.getCanonicalName());
    factory.addImport(Annotation.class.getCanonicalName());
    factory.addImplementedInterface(classType.getQualifiedSourceName());
    factory.addImplementedInterface(InterceptableProxy.class.getSimpleName());
    return factory;
  }

  private void createBody(SourceWriter src) {
    instance().create(src);
    int index = 0;
    for (JMethod method : classType.getOverridableMethods()) {
      if (findInterceptors(method).length > 0) {
        createInterceptedMethod(src, method, index++);
      }
      else {
        createDelegateMethod(src, method);
      }
    }
    createProxyMethods(src, index);
  }

  private Builder instance() {
    JClassType rpc = isRpcAsync();
    if (rpc != null) {
      String name = rpc.getQualifiedSourceName();
      return gwtCreate(name + RPC_ASYNC, name);
    }
    else {
      String name = findTargetIface().getQualifiedSourceName();
      return gwtCreate(name, name);
    }
  }
  
  private Builder gwtCreate(String res, String type) {
    return new RawBuilder("private final " + res + " _instance = GWT.create(" + type + ".class);");
  }
  
  private JClassType isRpcAsync() {
    TypeOracle oracle = classType.getOracle();
    String name = classType.getQualifiedSourceName();
    if (name.endsWith(RPC_ASYNC)) {
      String rpc = name.substring(0, name.length() - RPC_ASYNC.length());
      JClassType rpcType = oracle.findType(rpc);
      if (rpcType == null || rpcType.isInterface() == null) {
        return null;
      }
      JClassType rsType = oracle.findType(RemoteService.class.getCanonicalName());
      if (rsType == null) {
        return null;
      }
      return rpcType.isAssignableTo(rsType)? rpcType : null;
    }
    return null;
  }

  private JClassType findTargetIface() {
    TypeOracle oracle = classType.getOracle();
    JClassType interceptable = oracle.findType(Interceptable.class.getCanonicalName());
    ArrayList<JClassType> candidates = new ArrayList<JClassType>();
    for (JClassType type : classType.getImplementedInterfaces()) {
      if (!type.equals(interceptable)) {
        candidates.add(type);
      }
    }
    if (candidates.size() != 1) {
      String msg = "Could not decide the type to rebind.";
      throw new UncheckUnableToCompleteException(msg);
    }
    JClassType target = candidates.get(0);
    if (target.isAssignableTo(interceptable)) {
      String msg = "Cycle detected, a subtype of " + target.getQualifiedSourceName()
          + " implements Interceptable.";
      throw new UncheckUnableToCompleteException(msg);
    }
    return target;
  }

  private void createInterceptedMethod(SourceWriter src, JMethod method, int index) {
    src.println();
    new FieldBuilder(method, index, annotations).create(src);
    src.println();
    Builder invoke = interceptor(index, findInterceptors(method), 0, targetCall(method, true));
    Builder body = new ReturnBuilder(method.getReturnType(), invoke);
    new MethodBuilder(method, body, true).create(src);
  }

  private Builder interceptor(int method, Class<?>[] array, int index, Builder builder) {
    Builder child;
    if (index == array.length - 1) {
      child = builder;
    }
    else {
      JClassType object = classType.getOracle().getJavaLangObject();
      Builder next = interceptor(method, array, index + 1, builder);
      child = new ReturnBuilder(object, next);
    }
    return new InterceptorBuilder(method, array[index], child);
  }

  private void createDelegateMethod(SourceWriter src, JMethod method) {
    src.println();
    new MethodBuilder(method, targetCall(method, false), false).create(src);
  }

  private Builder targetCall(JMethod method, boolean isIntercepted) {
    String args = MethodBuilder.arguments(method, isIntercepted);
    String call =  "_instance." + method.getName() + "(" + args + ");";
    if (!method.getReturnType().equals(JPrimitiveType.VOID)) {
      return new RawBuilder("return " + call);
    }
    else if (isIntercepted) {
      return new RawBuilder(call, "return null;");
    }
    else {
      return new RawBuilder(call);
    }
  }
  
  private void createProxyMethods(SourceWriter src, int methods) {
    new ProxyBuilder(methods, true).create(src);
  }
}
